package simpledb.index;

import simpledb.common.Database;
import simpledb.common.DbException;
import simpledb.common.Type;
import simpledb.storage.BufferPool;
import simpledb.storage.Page;
import simpledb.storage.TupleDesc;
import simpledb.transaction.TransactionId;

/**
 * 每个 BTreePage 实例代表一个 B+ 树文件中的页面，它实现了 BufferPool 使用的 Page 接口。
 */
public abstract class BTreePage implements Page {

    // 当前页面是否被修改过（脏页）
    protected volatile boolean       dirty       = false;
    // 修改该页面的事务 ID
    protected volatile TransactionId dirtier     = null;

    // 索引中每个整数字段的大小（4 字节）
    protected final static int       INDEX_SIZE  = Type.INT_TYPE.getLen();

    // 页面唯一标识符
    protected final BTreePageId      pid;
    // 该页面存储元组的描述信息
    protected final TupleDesc        td;
    // 当前索引所基于的关键字段位置
    protected final int              keyField;

    // 父节点页面编号（对于根节点可以为 0）
    protected int                    parent;
    // 页面原始数据的备份，用于回滚操作
    protected byte[]                 oldData;
    // 数据锁对象，用于同步旧数据操作
    protected final Byte             oldDataLock = (byte) 0;

    /**
     * 从磁盘读取的字节数据创建一个 BTreeInternalPage。
     *
     * 页面格式包括：
     * - 一组 header 字节，表示哪些插槽正在使用；
     * - 若干 entry 插槽；
     * - 额外的父指针、一个额外子指针（m 个条目有 m+1 个子节点指针）；
     * - 所有子页面的类型（叶子或内部节点）。
     *
     * 具体来说，条目数量等于：
     * <p>
     * floor((BufferPool.getPageSize()*8 - extra bytes*8) / (entry size * 8 + 1))
     * </p>
     * 其中 entry size 是当前索引节点中条目的大小（key + child pointer），
     * 可通过关键字段和 Database.getCatalog().getTupleDesc() 获取。
     *
     * Header 字节数量等于：
     * <p>
     * ceiling((no. entry slots + 1) / 8)
     * </p>
     *
     * @param id - 此页面的唯一标识符
     * @param key - 当前索引使用的字段索引
     */
    public BTreePage(BTreePageId id, int key) {
        this.pid = id;
        this.keyField = key;
        this.td = Database.getCatalog().getTupleDesc(id.getTableId());
    }

    /**
     * 返回与此页面关联的页面 ID。
     */
    public BTreePageId getId() {
        return pid;
    }

    /**
     * 静态方法，生成一个对应于空页面的字节数组。
     * 用于向文件中添加新页面。将此方法的结果传递给 BTreeInternalPage 或 BTreeLeafPage 构造函数时，
     * 将创建一个没有有效条目的页面。
     *
     * @return 返回一个初始化为空的字节数组
     */
    public static byte[] createEmptyPageData() {
        int len = BufferPool.getPageSize(); // 获取页面大小，默认为 4096 字节
        return new byte[len]; // 初始化为全 0
    }

    /**
     * 获取当前页面的父节点 ID。
     * 如果当前页面是根节点，则返回根指针页面的 ID。
     *
     * @return 父节点的页面 ID
     */
    public BTreePageId getParentId() {
        if (parent == 0) {
            return BTreeRootPtrPage.getId(pid.getTableId()); // 如果是根节点，返回根指针页面 ID
        }
        return new BTreePageId(pid.getTableId(), parent, BTreePageId.INTERNAL); // 否则返回内部节点 ID
    }

    /**
     * 设置当前页面的父节点 ID。
     *
     * @param id - 新的父节点页面 ID
     * @throws DbException 如果提供的 ID 不合法
     */
    public void setParentId(BTreePageId id) throws DbException {
        if (id == null) {
            throw new DbException("父节点 ID 不能为空");
        }
        if (id.getTableId() != pid.getTableId()) {
            throw new DbException("设置父节点时表 ID 不匹配");
        }
        if (id.pgcateg() != BTreePageId.INTERNAL && id.pgcateg() != BTreePageId.ROOT_PTR) {
            throw new DbException("父节点必须是内部节点或根指针");
        }
        if (id.pgcateg() == BTreePageId.ROOT_PTR) {
            parent = 0; // 根指针页面用 0 表示
        } else {
            parent = id.getPageNumber(); // 否则记录实际页面号
        }
    }

    /**
     * 标记该页面是否为脏页，并记录修改它的事务 ID。
     */
    public void markDirty(boolean dirty, TransactionId tid) {
        this.dirty = dirty;
        if (dirty)
            this.dirtier = tid;
    }

    /**
     * 返回最后修改该页面的事务 ID。
     * 如果页面未被修改，则返回 null。
     */
    public TransactionId isDirty() {
        if (this.dirty)
            return this.dirtier;
        else
            return null;
    }

    /**
     * 返回当前页面中空插槽的数量。
     * 这是一个抽象方法，由具体子类实现。
     */
    public abstract int getNumEmptySlots();

    /**
     * 判断指定索引位置的插槽是否被使用。
     * 这是一个抽象方法，由具体子类实现。
     */
    public abstract boolean isSlotUsed(int i);
}
